<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">

    <title>Basic Lighting Shader — Alien.js</title>

    <link rel="preconnect" href="https://fonts.gstatic.com">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
    <link rel="stylesheet" href="../assets/css/style.css">

    <script type="module">
        import { BasicLightingMaterial, BufferGeometryLoader, BufferGeometryLoaderThread, Color, ColorManagement, DirectionalLight, EnvironmentTextureLoader, Group, HemisphereLight, ImageBitmapLoaderThread, LinearSRGBColorSpace, MathUtils, Mesh, MeshBasicMaterial, PanelItem, PerspectiveCamera, RepeatWrapping, Scene, SphereGeometry, TextureLoader, UI, Vector2, Vector3, WebGLRenderer, getFullscreenTriangle, getViewSize, ticker, tween } from '../../build/alien.three.js';

        const isDebug = /[?&]debug/.test(location.search);

        class MouseLight extends Group {
            constructor() {
                super();

                this.position.z = 2;

                if (isDebug) {
                    this.initDebug();
                }
            }

            initDebug() {
                const geometry = new SphereGeometry(0.125, 1, 1);

                const material = new MeshBasicMaterial({
                    color: 0xffffff,
                    wireframe: true
                });

                const mesh = new Mesh(geometry, material);
                this.add(mesh);
            }
        }

        class Polyhedron extends Group {
            constructor() {
                super();
            }

            async initGeometry() {
                const { loadBufferGeometry } = WorldController;

                const geometry = await loadBufferGeometry('polyhedron.json');

                this.geometry = geometry;
            }

            async initMaterial() {
                const { anisotropy, loadTexture } = WorldController;

                // Textures
                // const map = await loadTexture('uv.jpg');
                const map = await loadTexture('pbr/pitted_metal_basecolor.jpg');
                map.anisotropy = anisotropy;
                map.wrapS = RepeatWrapping;
                map.wrapT = RepeatWrapping;
                map.repeat.set(2, 1);

                const material = new BasicLightingMaterial({
                    map,
                    // lightPosition: new Vector3(1, 1, 1),
                    lightIntensity: 0.25
                });

                this.material = material;
            }

            initMesh() {
                const mesh = new Mesh(this.geometry, this.material);
                this.add(mesh);
            }

            // Public methods

            ready = async () => {
                await Promise.all([
                    this.initGeometry(),
                    this.initMaterial()
                ]);

                this.initMesh();
            };
        }

        class SceneView extends Group {
            constructor() {
                super();

                this.visible = false;

                this.initViews();
            }

            initViews() {
                this.polyhedron = new Polyhedron();
                this.add(this.polyhedron);

                this.light = new MouseLight();
                this.add(this.light);
            }
        }

        class SceneController {
            static init(view) {
                this.view = view;

                // Mouse light
                this.mouse = new Vector2();
                this.target = new Vector2();
                this.lightPosition = new Vector3();
                this.lerpSpeed = 0.25;

                this.addListeners();
            }

            static addListeners() {
                window.addEventListener('pointermove', this.onPointerMove);
            }

            // Event handlers

            static onPointerMove = ({ clientX, clientY }) => {
                if (!this.view.visible) {
                    return;
                }

                this.target.x = (clientX / document.documentElement.clientWidth) * 2 - 1;
                this.target.y = 1 - (clientY / document.documentElement.clientHeight) * 2;
            };

            // Public methods

            static resize = () => {
                const { getViewSize } = WorldController;

                const { x, y } = getViewSize(this.view.light.position.z);

                this.halfWidth = x / 2;
                this.halfHeight = y / 2;
            };

            static update = () => {
                if (!this.view.visible) {
                    return;
                }

                this.mouse.lerp(this.target, this.lerpSpeed);

                this.lightPosition.x = this.mouse.x * this.halfWidth;
                this.lightPosition.y = this.mouse.y * this.halfHeight;
                this.lightPosition.z = this.view.light.position.z;

                this.view.polyhedron.material.uniforms.uLightPosition.value.copy(this.lightPosition);
                this.view.light.position.copy(this.lightPosition);
            };

            static animateIn = () => {
                this.view.visible = true;
            };

            static ready = () => this.view.polyhedron.ready();
        }

        class PanelController {
            static init(view, ui) {
                this.view = view;
                this.ui = ui;

                this.initPanel();
            }

            static initPanel() {
                const { polyhedron } = this.view;

                const items = [
                    {
                        name: 'FPS'
                    },
                    {
                        type: 'divider'
                    },
                    {
                        type: 'slider',
                        name: 'Int',
                        min: 0,
                        max: 1,
                        step: 0.01,
                        value: polyhedron.material.uniforms.uLightIntensity.value,
                        callback: value => {
                            polyhedron.material.uniforms.uLightIntensity.value = value;
                        }
                    }
                ];

                items.forEach(data => {
                    this.ui.addPanel(new PanelItem(data));
                });
            }
        }

        class RenderManager {
            static init(renderer, scene, camera) {
                this.renderer = renderer;
                this.scene = scene;
                this.camera = camera;
            }

            // Public methods

            static resize = (width, height, dpr) => {
                this.renderer.setPixelRatio(dpr);
                this.renderer.setSize(width, height);
            };

            static update = () => {
                this.renderer.render(this.scene, this.camera);
            };
        }

        class CameraController {
            static init(camera) {
                this.camera = camera;

                this.mouse = new Vector2();
                this.target = new Vector2();

                // Motion control
                this.group = new Group();
                this.innerGroup = new Group();
                this.group.add(this.innerGroup);
                this.group.matrixAutoUpdate = false;
                this.innerGroup.matrixAutoUpdate = false;

                // Start position
                this.innerGroup.position.copy(this.camera.position);

                this.rotation = 0.0002;
                this.lerpSpeed = 0.05;
                this.multiplier = 1;
                this.enabled = false;
                this.prevent = true;

                this.addListeners();
            }

            static addListeners() {
                window.addEventListener('pointermove', this.onPointerMove);
            }

            // Event handlers

            static onPointerMove = ({ clientX, clientY }) => {
                if (this.prevent) {
                    return;
                }

                this.mouse.x = clientX - this.halfWidth;
                this.mouse.y = clientY - this.halfHeight;

                this.target.x = MathUtils.degToRad(-360) + this.mouse.x * this.rotation * this.multiplier;
                this.target.y = this.mouse.y * this.rotation * this.multiplier;
            };

            // Public methods

            static resize = (width, height) => {
                this.camera.aspect = width / height;
                this.camera.updateProjectionMatrix();

                this.halfWidth = width / 2;
                this.halfHeight = height / 2;

                if (width < height) {
                    this.camera.position.z = 10;
                    this.multiplier = 2;
                } else {
                    this.camera.position.z = 8;
                    this.multiplier = 1;
                }

                this.innerGroup.position.z = this.camera.position.z;
            };

            static update = () => {
                if (!this.enabled) {
                    return;
                }

                this.group.rotation.x += (this.target.y - this.group.rotation.x) * this.lerpSpeed;
                this.group.rotation.y += (this.target.x - this.group.rotation.y) * this.lerpSpeed;

                this.updateCamera();
            };

            static updateCamera = () => {
                this.group.updateMatrix();
                this.innerGroup.updateMatrix();
                this.group.updateMatrixWorld();
                this.innerGroup.matrixWorld.decompose(this.camera.position, this.camera.quaternion, this.camera.scale);
            };

            static animateIn = () => {
                this.enabled = true;

                tween(this.target, { x: MathUtils.degToRad(-360) }, 4200, 'easeInOutQuart', () => {
                    this.prevent = false;
                });
            };
        }

        class WorldController {
            static init() {
                this.initWorld();
                this.initLights();
                this.initLoaders();
                this.initEnvironment();

                this.addListeners();
            }

            static initWorld() {
                this.renderer = new WebGLRenderer({
                    powerPreference: 'high-performance',
                    antialias: true
                });

                // Output canvas
                this.element = this.renderer.domElement;

                // Disable color management
                ColorManagement.enabled = false;
                this.renderer.outputColorSpace = LinearSRGBColorSpace;

                // 3D scene
                this.scene = new Scene();
                this.scene.background = new Color(0x060606);
                this.camera = new PerspectiveCamera(30);
                this.camera.near = 0.5;
                this.camera.far = 40;
                this.camera.position.z = 8;
                this.camera.lookAt(this.scene.position);

                // Global geometries
                this.screenTriangle = getFullscreenTriangle();

                // Global uniforms
                this.resolution = { value: new Vector2() };
                this.texelSize = { value: new Vector2() };
                this.aspect = { value: 1 };
                this.time = { value: 0 };
                this.frame = { value: 0 };

                // Global settings
                this.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
            }

            static initLights() {
                this.scene.add(new HemisphereLight(0x606060, 0x404040, 3));

                const light = new DirectionalLight(0xffffff, 2);
                light.position.set(0.6, 0.5, 1);
                this.scene.add(light);
            }

            static initLoaders() {
                this.textureLoader = new TextureLoader();
                this.textureLoader.setPath('../assets/textures/');

                this.environmentLoader = new EnvironmentTextureLoader(this.renderer);
                this.environmentLoader.setPath('../assets/textures/env/');

                this.bufferGeometryLoader = new BufferGeometryLoader();
                this.bufferGeometryLoader.setPath('../assets/geometry/');
            }

            static async initEnvironment() {
                this.scene.environment = await this.loadEnvironmentTexture('jewelry_black_contrast.jpg');
                this.scene.environmentIntensity = 0.5;
            }

            static addListeners() {
                this.renderer.domElement.addEventListener('touchstart', this.onTouchStart);
            }

            // Event handlers

            static onTouchStart = e => {
                e.preventDefault();
            };

            // Public methods

            static resize = (width, height, dpr) => {
                width = Math.round(width * dpr);
                height = Math.round(height * dpr);

                this.resolution.value.set(width, height);
                this.texelSize.value.set(1 / width, 1 / height);
                this.aspect.value = width / height;
            };

            static update = (time, delta, frame) => {
                this.time.value = time;
                this.frame.value = frame;
            };

            static ready = () => Promise.all([
                this.textureLoader.ready(),
                this.environmentLoader.ready()
            ]);

            // Global handlers

            static getTexture = (path, callback) => this.textureLoader.load(path, callback);

            static loadTexture = path => this.textureLoader.loadAsync(path);

            static loadEnvironmentTexture = path => this.environmentLoader.loadAsync(path);

            static getBufferGeometry = (path, callback) => this.bufferGeometryLoader.load(path, callback);

            static loadBufferGeometry = path => this.bufferGeometryLoader.loadAsync(path);

            static getViewSize = object => getViewSize(this.camera, object);
        }

        class App {
            static async init() {
                this.initThread();
                this.initWorld();
                this.initViews();
                this.initControllers();

                this.addListeners();
                this.onResize();

                await SceneController.ready();
                await WorldController.ready();

                this.initPanel();

                CameraController.animateIn();
                SceneController.animateIn();
            }

            static initThread() {
                ImageBitmapLoaderThread.init();
                BufferGeometryLoaderThread.init();
            }

            static initWorld() {
                WorldController.init();
                document.body.appendChild(WorldController.element);
            }

            static initViews() {
                this.view = new SceneView();
                WorldController.scene.add(this.view);

                this.ui = new UI({ fps: true });
                this.ui.animateIn();
                document.body.appendChild(this.ui.element);
            }

            static initControllers() {
                const { renderer, scene, camera } = WorldController;

                CameraController.init(camera);
                SceneController.init(this.view);
                RenderManager.init(renderer, scene, camera);
            }

            static initPanel() {
                PanelController.init(this.view, this.ui);
            }

            static addListeners() {
                window.addEventListener('resize', this.onResize);
                ticker.add(this.onUpdate);
                ticker.start();
            }

            // Event handlers

            static onResize = () => {
                const width = document.documentElement.clientWidth;
                const height = document.documentElement.clientHeight;
                const dpr = window.devicePixelRatio;

                WorldController.resize(width, height, dpr);
                CameraController.resize(width, height);
                SceneController.resize();
                RenderManager.resize(width, height, dpr);
            };

            static onUpdate = (time, delta, frame) => {
                WorldController.update(time, delta, frame);
                CameraController.update();
                SceneController.update();
                RenderManager.update(time, delta, frame);
                this.ui.update();
            };
        }

        App.init();
    </script>
</head>
<body>
</body>
</html>
